/**
 * Copyright (c) 2009 Anders Krarup (andkrup@gmail.com)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.andkrup.easygame.game {
	import com.andkrup.easygame.game.objects.ICharacterGroup;	
	import com.andkrup.easygame.externalmodules.ExternalModuleAccept;	
	import com.andkrup.easygame.events.ExternalModuleEvent;	
	import com.andkrup.easygame.externalmodules.IExternalModuleFactory;	
	
	import flash.display.DisplayObject;	
	import flash.display.LoaderInfo;	
	import flash.events.IOErrorEvent;	
	import flash.net.URLRequest;	
	import flash.events.Event;	
	import flash.display.Loader;	
	
	import com.andkrup.easygame.externalmodules.ExternalModuleContainer;	
	import com.andkrup.easygame.externalmodules.ExternalModule;	
	import com.andkrup.easygame.events.GameEngineErrorEvent;	
	import com.andkrup.easygame.events.GameEngineEvent;	
	import com.andkrup.easygame.game.objects.MapFactory;	
	import com.andkrup.easygame.game.objects.IMapFactory;	
	import com.andkrup.easygame.config.GameConfiguration;	
	import com.andkrup.easygame.data.SaveGame;		import com.andkrup.events.LoaderEvent;	
	import com.andkrup.serialization.IParser;	
	import com.andkrup.serialization.IParserProvider;	
	import com.andkrup.easygame.game.commands.IGameCommand;	
	
	import flash.utils.getDefinitionByName;	
	
	import com.andkrup.easygame.config.KeyConfiguration;	
	import com.andkrup.comm.ResourceManager;	
	import com.adobe.net.URI;	
	import com.andkrup.easygame.ui.IViewPort;
	
	import flash.events.EventDispatcher;	
	
	import com.andkrup.easygame.GamePackage;

	/**
	 * @author Andkrup
	 * 
	 * The GameEngine must have a IViewPort to render gamecontent in
	 */
	public class EasyGameEngine extends EventDispatcher implements GameEngine, ExternalModuleContainer {
		private static var _inst:EasyGameEngine;
		private var currGamePackage : GamePackage;
		private var currentGame : Game;
		private var viewport : IViewPort;
		private var loadedAssets : Object;
		private var keyConfig : KeyConfiguration;
		
		public var assetsParserProvider:IParserProvider;
		private var savegameManager : ISaveGameManager;
		private var gameConfig : GameConfiguration;
		private var mapProvider : IMapFactory;
		private var missionProvider : IMissionFactory;
		private var modules:Array;
		private var activeModules:Object;
		private var currentMission:IMission;

		public static function get instance():EasyGameEngine{
			if(!_inst){
				_inst = new EasyGameEngine(new GameEngineConstructor());
			}
			return _inst;
		}
		
		public function get hasGame():Boolean{
			return null!=currentGame;
		}
		public function get gameRunning():Boolean{
			if(!currentGame){
				return false;
			}
			return currentGame.running;
		}
		public function get currentGamePackage():GamePackage{
			return currGamePackage;
		}
		
		public function get viewPort():IViewPort{
			return viewport;
		}
		public function set viewPort(value:IViewPort):void{
			if(viewport && !value){
				// remove listeners before deleting reference
				removeViewPortListeners();
			}
			viewport = value;
			if(viewport){
				addViewPortListeners();
			}
		}
		
		
		public function get currentGameCharacterGroup():ICharacterGroup{
			if(!currentGame){
				return null;
			}
			return currentGame.characterGroup;
		}
		public function set currentGameCharacterGroup(value:ICharacterGroup):void{
			if(!currentGame){
				return;
			}
			currentGame.characterGroup = value;
		}

		
		public function EasyGameEngine(ctr:GameEngineConstructor){
			assetsParserProvider = new GameAssetsParserProvider();
			savegameManager = SaveGameUtility.instance;
			modules = [];
			reset();
			// prep to listen for loading assets events
			ResourceManager.instance.addEventListener(LoaderEvent.ITEM_COMPLETE, onAssetLoaded);
			ResourceManager.instance.addEventListener(LoaderEvent.QUEUE_COMPLETE, onAllAssetsLoaded);
			
			ctr = undefined;
		}

		/**
		 * Load a gamepackage.
		 * 
		 * This method does the following:
		 * - loads all assets found in the package
		 * - creates the needed class-factories
		 * 
		 * When all external assets have been loaded, the event
		 * QueueEvent.QUEUE_COMPLETE is dispatched.
		 * 
		 * Throws an Error if gamePackage is null or undefined
		 */
		public function loadGamePackage(gamePackage:GamePackage):void{
			if(!gamePackage){
				throw new Error("gamePackage is null");
			}
			currGamePackage = gamePackage;
			// include definitions in package:
			currGamePackage.includeDefinitions();
			// get the mapProvider
			if(currGamePackage.mapFactory){
				mapProvider = currGamePackage.mapFactory;
			}
			else{
				// or use the default
				mapProvider = new MapFactory();
			}
			// get the missionProvider
			if(currGamePackage.missionFactory){
				missionProvider = currGamePackage.missionFactory;
			}
			else{
				// or use the default
				missionProvider = new MissionFactory();
			}
			
			var arr:Array;
			var uri:URI;
			// find and load all assets (xml files containing information about various resources)
			arr = currGamePackage.externalGameAssets;
			if(arr){
				for each(uri in arr){
					loadedAssets[uri.toString()] = false;
					ResourceManager.instance.loadResource(uri);
				}
			}
			
			// find and load all external modules (swf files containing type definitions and a main module subclass)
			arr = currGamePackage.modules;
			if(arr){
				var loader:Loader;
				for each(uri in arr){
//					loadedAssets[uri.toString()] = false;
					loader = new Loader();
					loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoaderComplete);
					loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onLoaderIOError);
					trace("EasyGameEngine:> loadGamePackage() loading: "+uri);
					loader.load(new URLRequest(uri.toString()));
				}
			}
		}
		
		private function onLoaderIOError(event:IOErrorEvent):void {
			trace("EasyGameEngine:> onLoaderIOError() event.target: "+event.target);
		}

		private function onLoaderComplete(event:Event):void {
			trace("EasyGameEngine:> onLoaderComplete("+event.type+") event.target: "+event.target);
			var loaderinfo:LoaderInfo = event.target as LoaderInfo;
			if(loaderinfo){
				trace("  content: "+typeof(loaderinfo.content));
				modules.push(loaderinfo.content);
			}
		}

		public function unloadGamePackage(reset:Boolean = true):void{
			currGamePackage = undefined;
			if(reset){
				this.reset();
			}
		}
		/**
		 * Load a savegame
		 */
		public function loadGame(uri:URI, id:String, local:Boolean = true):Boolean{
			var sg:SaveGame = savegameManager.loadSaveGame(uri, id);
			return null!=sg;
		}
		/**
		 * Create and store a savegame
		 */
		public function saveGame(uri:URI, id:String = null, local:Boolean = true):Boolean{
			var rt:Boolean = savegameManager.createSaveGame(currentGame, uri, id);
			return rt;
		}
		/**
		 * Get a list of savegames.
		 * 
		 * Returns a list of SaveGame objects
		 */
		public function getSaveGames(local:Boolean = true):Array{
			return [];
		}
		/**
		 * Loads external data to be used in the context defined by type
		 */
		public function loadGameData(url:URI, type:GameDataType):void{
		}
		/**
		 * Saves data externally within the context defined by type
		 */
		public function saveGameData(url:URI, type:GameDataType):void{
		}
		/**
		 * Clears data found in the context defined by type, local data or
		 * external data.
		 */
		public function clearGameData(url:URI, type:GameDataType, resetLocal:Boolean = true, resetExternal:Boolean = false):void{
		}
		/**
		 * Adds additional assets into the GameEngine. External assets must be
		 * loaded in the application before being added with this method.
		 */
		public function loadAssets():void{
			
		}
		
		public function createNewGame():void{
			if(currentGame){
				throw new Error("game exists");
			}
			
			currentGame = new Game(gameConfig);
		}
		
		public function startCurrentGame():void{
			currentGame.start();
		}
		public function abortCurrentGame():void{
			if(currentGame){
				currentGame.stop();
			}
			if(keyConfig){
				keyConfig.enabled = false;
			}
		}
		
		public function loadMission(missionId:String):void {
			if(currentMission){
				unloadMission();
			}
			currentMission = missionProvider.getMission(missionId);
			currentMission.map = mapProvider.getMap(missionId);
			currentMission.addEventListener(Event.INIT, onMissionInit, false, 0, true);
			
			currentMission.init();
		}
		
		private function onMissionInit(event:Event):void {
			currentMission.removeEventListener(Event.INIT, onMissionInit);
			dispatchEvent(new GameEngineEvent(GameEngineEvent.MISSION_READY));
		}

		public function startMission():void{
			currentGame.viewport = viewPort;
			currentGame.startMission(currentMission);
		}
		public function unloadMission():void{
			currentMission = undefined;
		}
		
		public function addModule(module:ExternalModule):void{
			if(!containsModule(module)){
				modules.push(module);
			}
		}
		/**
		 * NB! This checks in the activeModules array
		 */
		public function containsModule(module:ExternalModule):Boolean{
			var mod:ExternalModule;
			for each(mod in activeModules){
				if(mod==module){
					return true;
				}
			}
			return false;
		}
		public function removeModule(module:ExternalModule):void{
			var l:uint = modules.length;
			for(var i:uint = 0; i < l; i++){
				if(module==modules[i]){
					modules.splice(i, 1);
					break;
				}
			}
		}
		/**
		 * TODO: how do you know that the factory is called ExternalModuleFactory?
		 * 
		 * Thid infotmation should be input via the GamePackage. Maybe by loading
		 * a Module-datafile containing this info + the path to the rtl swf
		 */
		public function activateModule(name:String):void{
			if(!activeModules){
				activeModules = {};
			}
			if(activeModules[name]){
				throw new Error("EasyGameEngine:> loadModule() module already loaded ("+name+")");
			}
			// after gamepackage has been checked for external modules to load, this method should be able to return a module, if asked for
			var mod:ExternalModule;
			var content:DisplayObject;
			var factory:IExternalModuleFactory;
			for each(content in modules){
				trace("EasyGameEngine:> getModuleByName() getting appDomain: "+content.loaderInfo.applicationDomain.hasDefinition("ExternalModuleFactory"));
				if(content.loaderInfo.applicationDomain.hasDefinition("ExternalModuleFactory")){
					var cls:Class = content.loaderInfo.applicationDomain.getDefinition("ExternalModuleFactory") as Class;
					factory = new cls();
					if(factory){
						mod = factory.getModuleByName(name);
						if(mod.name.toLowerCase()==name.toLowerCase()){
							activeModules[name] = mod;
							mod.addEventListener(ExternalModuleEvent.MODULE_ACTIVATED, onModuleActivated, false, 0, true);
							mod.init(this);
							return;
						}
					}
				}
			}
		}
		
		private function onModuleActivated(event:ExternalModuleEvent):void {
			var mod:ExternalModule = event.target as ExternalModule;
			mod.removeEventListener(ExternalModuleEvent.MODULE_ACTIVATED, onModuleActivated);
			dispatchEvent(new ExternalModuleEvent(ExternalModuleEvent.MODULE_ACTIVATED, event.moduleName));
		}

		public function isActive(name:String):Boolean{
			if(!activeModules[name]){
				return false;
			}
			return activeModules[name] is ExternalModule;
		}
		
		public function deactivateModule(name:String):void{
			var mod:ExternalModule;
			if(activeModules[name]){
				mod = activeModules[name] as ExternalModule;
				mod.kill();
				activeModules[name] = undefined;
				dispatchEvent(new ExternalModuleEvent(ExternalModuleEvent.MODULE_DEACTIVATED, name));
			}
		}
		
		/**
		 * The main method of using an activated module.
		 * 
		 * Module-developers needs to put return values into a input-parameter
		 * so, when expecting a return value from invoking call(), the method that
		 * is being called, must take at least 1 parameter
		 */
		public function call(method:String, ...args):void{
			var f:Function;
			for each(var mod:ExternalModule in activeModules){
				f = mod.call;
				args.splice(0, 0, method);
				if(f.apply(mod, args)==ExternalModuleAccept.BREAK) {
					break;
				}
			}
		}
		
		private function reset():void{
			loadedAssets = {};
		}
		
		private function addViewPortListeners() : void {
			viewport.addEventListener(Event.ADDED_TO_STAGE, onLoadViewPort, false, 0, true);
		}
		private function removeViewPortListeners() : void {
			viewport.removeEventListener(Event.ADDED_TO_STAGE, onLoadViewPort);
		}
		
		private function onLoadViewPort(event:Event):void {
			viewport.removeEventListener(Event.ADDED_TO_STAGE, onLoadViewPort);
		}

		public function onKeyCommand(char:String, type:String, shift:Boolean = false, ctrl:Boolean = false, alt:Boolean = false) : void {
			// find appropriate IGameCommand from keyconfig and feed it to a list of command-digesters (TODO: very close to being an event???)
			var cmd:IGameCommand = keyConfig.getGameCommand(char, type);
			if(cmd){
//				trace("EasyGameEngine:> onKeyUp() cmd: "+cmd);
				if(!digestCommand(cmd)){
					trace("EasyGameEngine:> onKeyCommand() gameengine passed command");
					if(!currentGamePackage.commandHub.digestCommand(cmd)){
						trace("EasyGameEngine:> onKeyCommand() gamepackage passed command");
						var b:Boolean = currentGame.digestCommand(cmd);
						trace("EasyGameEngine:> onKeyCommand() game passed command: "+b);
					}
				}
			}
		}
		/**
		 * Console hook. Enable console-scripting in your game
		 */
		public function evaluateCommand(cmdName:String, ...args):void{
			var cmdCls:Class = getDefinitionByName(cmdName) as Class;
			if(cmdCls){
				var cmd:IGameCommand = new CommandFactory(cmdCls, args).newInstance() as IGameCommand;
				if(cmd){
					if(!digestCommand(cmd)){
						if(!currentGamePackage.commandHub.digestCommand(cmd)){
							currentGame.digestCommand(cmd);
						}
					}
				}
			}
		}
		/**
		 * Evaluate a command and return true if this instance wants to digest
		 * the command
		 */
		public function digestCommand(command:IGameCommand):Boolean{
			return false;
		}
		
		private function onAssetLoaded(le: LoaderEvent) : void {
			var datatype:String = typeof(le.currentItem);
			var parser:IParser = assetsParserProvider.getParser(le.currentItem);
			if(parser){
				loadedAssets[le.currentItemSrc.toString()] = parser.decode(le.currentItem);
			}
			trace("GameEngine:> onAssetLoaded() item: " + datatype+", parser: "+parser+", asset: "+loadedAssets[le.currentItemSrc.toString()]);
		}

		private function onAllAssetsLoaded(le : LoaderEvent) : void {
			trace("GameEngine:> onAllAssetsLoaded() ");
			
			for(var k:String in loadedAssets){
				// find a keyconfig - required
				trace("GameEngine:> onAllAssetsLoaded() loadedAssets["+k+"]: "+loadedAssets[k]);
				if(loadedAssets[k] is KeyConfiguration){
					keyConfig = loadedAssets[k] as KeyConfiguration;
				}
				// find a gameconfig - required
				if(loadedAssets[k] is GameConfiguration){
					gameConfig = loadedAssets[k] as GameConfiguration;
				}
				// find savegames - if present
//				if(loadedAssets[k] is KeyConfiguration){
//					keyConfig = loadedAssets[k] as KeyConfiguration;
//				}
			}
			// find maps - required
			if(!mapProvider){
				dispatchEvent(new GameEngineErrorEvent(GameEngineErrorEvent.PACKAGE_ERROR, "IMapProvider is null"));
			}
			// find missions - required
			if(!missionProvider){
				dispatchEvent(new GameEngineErrorEvent(GameEngineErrorEvent.PACKAGE_ERROR, "IMissionProvider is null"));
			}
			if(!viewport){
				dispatchEvent(new GameEngineErrorEvent(GameEngineErrorEvent.PACKAGE_ERROR, "viewPort is null"));
			}
			if(!keyConfig){
				dispatchEvent(new GameEngineErrorEvent(GameEngineErrorEvent.PACKAGE_ERROR, "KeyConfiguration is null"));
			}
			if(!gameConfig){
				dispatchEvent(new GameEngineErrorEvent(GameEngineErrorEvent.PACKAGE_ERROR, "GameConfiguration is null"));
			}
			dispatchEvent(new GameEngineEvent(GameEngineEvent.PACKAGE_READY));
		}
	}
}
class GameEngineConstructor{}